We've
now seen several examples of plugin methods, some of which take
explicit parameters, and some of which do not. As we have explored, the
keyword this is always available to
provide context for the method, but we can also supply additional
information to influence the method's operation. So far, the parameters
have been few, but of course this list could grow large. There are
several tricks we can use to manage our method parameters and make life
easier for those using our plugins.
As our example, we'll start
with a plugin method that provides a shadow on a block of text. Our
technique will be similar to that used for the fading effect on the
news rotator : we will use a number of elements that are
partially transparent, overlaid in different positions on the page.
jQuery.fn.shadow = function() {
return this.each(function() {
var $originalElement = jQuery(this);
for (var i = 0; i < 5; i++) {
$originalElement
.clone()
.css({
position: 'absolute',
left: $originalElement.offset().left + i,
top: $originalElement.offset().top + i,
margin: 0,
zIndex: -1,
opacity: 0.1
})
.appendTo('body');
}
});
};
For each element this
method is called on, we make a number of clones of the element,
adjusting their opacity. These clones are positioned absolutely, at
varying offsets from the original element.
As usual, we'll test our plugin using some simple HTML:
<body>
<h1>The quick brown fox jumps over the lazy dog.</h1>
</body>
For the moment, our plugin takes no parameters, so calling the method is simple:
$(document).ready(function() {
$('h1').shadow();
});
Simple parameters
Now we can introduce some
flexibility to the plugin method. The operation of the method relies on
several numeric values that the user might want to modify. We can make
these into parameters so they can be changed on demand.
jQuery.fn.shadow = function(slices, opacity, zIndex) {
return this.each(function() {
var $originalElement = jQuery(this);
for (var i = 0; i < slices; i++) {
$originalElement
.clone()
.css({
position: 'absolute',
left: $originalElement.offset().left + i,
top: $originalElement.offset().top + i,
margin: 0,
zIndex: zIndex,
opacity: opacity
})
.appendTo('body');
}
});
};
Now, when calling our method, we must provide these three values.
$(document).ready(function() {
$('h1').shadow(10, 0.1, -1);
});
Our new parameters work
as anticipated—the shadow is longer, using twice as many
slices as before—but the method interface is less than ideal.
These three numbers are easily confused, and their order cannot be
logically deduced. It would be an improvement to label the parameters,
for the benefit of both the person writing the method call, and anyone
who later wishes to read and interpret it.
Parameter maps
We have seen many examples in the jQuery API of maps
being provided as method parameters. This can be a much friendlier way
to expose options to a plugin user than the simple parameter list we
just used. A map provides a visual label for each parameter, and also
makes the order of the parameters irrelevant. In addition, any time we
can mimic the jQuery API in our plugins, we should do so to increase
consistency and therefore ease-of-use.
jQuery.fn.shadow = function(opts) {
return this.each(function() {
var $originalElement = jQuery(this);
for (var i = 0; i < opts.slices; i++) {
$originalElement
.clone()
.css({
position: 'absolute',
left: $originalElement.offset().left + i,
top: $originalElement.offset().top + i,
margin: 0,
zIndex: opts.zIndex,
opacity: opts.opacity
})
.appendTo('body');
}
});
};
All we have changed to
enable our new interface is the way each parameter is referenced;
instead of having a separate variable name, each value is accessed as a
property of the opts argument to the function.
Calling this method now requires a map of values rather than three individual numbers:
$(document).ready(function() {
$('h1').shadow({
slices: 5,
opacity: 0.25,
zIndex: -1
});
});
The purpose of each parameter is now obvious from a quick glance at the method call.
Default parameter values
As the number of
parameters for a method grows, it becomes less likely that we will
always want to specify each one. A sensible set of default values
can make a plugin interface much more usable. Fortunately, using a map
for our parameters helps with this task as well; it is simple to omit
any item from the map and replace it with a default.
jQuery.fn.shadow = function(options) {
var defaults = {
slices: 5,
opacity: 0.1,
zIndex: -1
};
var opts = jQuery.extend(defaults, options);
return this.each(function() {
var $originalElement = jQuery(this);
for (var i = 0; i < opts.slices; i++) {
$originalElement
.clone()
.css({
position: 'absolute',
left: $originalElement.offset().left + i,
top: $originalElement.offset().top + i,
margin: 0,
zIndex: opts.zIndex,
opacity: opts.opacity
})
.appendTo('body');
}
});
};
Here, we have defined a new map, called defaults, within our method definition. The utility function $.extend() lets us take the options map provided as an argument and use it to override the items in defaults, leaving omitted items alone.
We still call our method using a map, but now we can specify only the parameters that we want to differ from their defaults:
$(document).ready(function() {
$('h1').shadow({
opacity: 0.05
});
});
Unspecified parameters use their default values. The $.extend() method even accepts null values, so if the default parameters are all acceptable, our method can be called very simply without errors:
$(document).ready(function() {
$('h1').shadow();
});
Callback functions
Of course, some method
parameters can be quite a bit more complicated than a simple numeric
value. One common parameter type we have seen frequently throughout the
jQuery API is the callback function.
Callback functions can lend a large amount of flexibility to a plugin
without requiring a great deal of preparation when creating the plugin.
To employ a callback
function in our method, we need simply accept the function object as a
parameter and call that function where appropriate in our method
implementation. As an example, we can extend our text shadow method to
allow the user to customize the position of the shadow relative to the
text.
jQuery.fn.shadow = function(options) {
var defaults = {
slices: 5,
opacity: 0.1,
zIndex: -1,
sliceOffset: function(i) {
return {x: i, y: i};
}
};
var opts = jQuery.extend(defaults, options);
return this.each(function() {
var $originalElement = jQuery(this);
for (var i = 0; i < opts.slices; i++) {
var offset = opts.sliceOffset(i);
$originalElement
.clone()
.css({
position: 'absolute',
left: $originalElement.offset().left
+ offset.x,
top: $originalElement.offset().top
+ offset.y,
margin: 0,
zIndex: opts.zIndex,
opacity: opts.opacity
})
.appendTo('body');
}
});
};
Each slice of the shadow
has a different offset from the original text. Before, this offset has
simply been equal to the index of the slice. Now, though, we're
calculating the offset using the sliceOffset()
function, which is a parameter that the user can override. So, for
example, we could provide negative values for the offset in both
dimensions:
$(document).ready(function() {
method parameterscall-back function$('h1').shadow({
sliceOffset: function(i) {
return {x: -i, y: -2*i};
}
});
});
This will cause the shadow to be cast up and to the left rather than down and to the right:
The callback allows simple
modifications to the shadow's direction, or much more sophisticated
positioning if the plugin user supplies the appropriate callback. If
the callback is not specified, then the default behavior is once again
used.
Customizable defaults
We can improve the
experience of using our plugins by providing reasonable default values
for our method parameters, as we have seen. However, sometimes it can
be difficult to predict what a reasonable default value will be. If a
script will be calling our plugin multiple times with a different set
of parameters than we set as the defaults, the ability to customize
these defaults could significantly reduce the amount of code that needs
to be written.
To make the defaults
customizable, we need to move them out of our method definition and
into a location that is accessible by outside code:
jQuery.fn.shadow = function(options) {
var opts = jQuery.extend({},
jQuery.fn.shadow.defaults, options);
return this.each(function() {
var $originalElement = jQuery(this);
for (var i = 0; i < opts.slices; i++) {
var offset = opts.sliceOffset(i);
$originalElement
.clone()
.css({
position: 'absolute',
left: $originalElement.offset().left + offset.x,
top: $originalElement.offset().top + offset.y,
margin: 0,
zIndex: opts.zIndex,
opacity: opts.opacity
})
.appendTo('body');
}
});
};
jQuery.fn.shadow.defaults = {
slices: 5,
opacity: 0.1,
zIndex: -1,
sliceOffset: function(i) {
return {x: i, y: i};
}
};
The defaults are now in the namespace of the shadow plugin, and can be directly referred to with $.fn.shadow.defaults. Our call to $.extend() had to change to accommodate this as well. Since we are now reusing the same defaults map for every call to .shadow(), we can't allow $.extend() to modify it. Instead, we provide an empty map {} as the first argument to $.extend(), and it is this new object that gets modified.
Now code that uses our plugin can change the defaults that all subsequent calls to .shadow() will use. Options can also still be supplied at the time the method is invoked.
$(document).ready(function() {
$.fn.shadow.defaults.slices = 10;
$('h1').shadow({
sliceOffset: function(i) {
return {x: -i, y: i};
}
});
});
This script will create a
shadow with 10 slices, because that is the new default value, but will
also cast the shadow left and down, due to the sliceOffset callback that is provided along with the method call.